package org.vaadin.dotmap.client.ui;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Image;
import com.vaadin.terminal.gwt.client.BrowserInfo;

public class VDotMap extends AbsolutePanel {

    public static final int ZINDEX_UNDER = 0;
    public static final int ZINDEX_MAP = 2;
    public static final int ZINDEX_OVER = 3;

    public static final String CLASSNAME = "v-dotmap";

    protected String width = null;
    protected String height = null;

    protected int topCrop = 0;

    protected static int FADE_ANIMATION_INTERVAL = 50; // == 20 fps

    protected Image mapImg;

    protected int mapWidth = -1;
    protected int mapHeight = -1;

    protected List<Highlight> highlightQueue;
    protected boolean loaded = false;

    protected List<MapLoadListener> mapLoadListeners = new LinkedList<MapLoadListener>();

    public VDotMap() {
        setStyleName(CLASSNAME);

        mapImg = new Image();
        mapImg.addLoadHandler(new LoadHandler() {
            public void onLoad(LoadEvent event) {
                int nw = mapImg.getWidth();
                int nh = mapImg.getHeight();
                boolean changed = (nw != mapWidth && "".equals(width))
                        || (nh != mapHeight && "".equals(height));
                mapWidth = nw;
                mapHeight = nh;

                if (changed) {
                    if ("".equals(width)) {
                        setWidth(mapWidth + "px");
                    }
                    if ("".equals(height)) {
                        setHeight(mapHeight + "px");
                    }

                    fireMapLoadListeners();
                }

                loaded = true;
                if (highlightQueue != null) {
                    for (Highlight h : highlightQueue) {
                        showHighlight(h);
                    }
                    highlightQueue = null;
                }
            }
        });
        add(mapImg, 0, 0);
        DOM.setIntStyleAttribute(mapImg.getElement(), "zIndex", ZINDEX_MAP);

    }

    private void fireMapLoadListeners() {
        for (MapLoadListener l : mapLoadListeners) {
            l.loaded();
        }
    }

    public void addListener(MapLoadListener l) {
        mapLoadListeners.add(l);
    }

    public void removeListener(MapLoadListener l) {
        mapLoadListeners.remove(l);
    }

    // we're going to guess that the new highlight is the same size as the last,
    // and adjust in the timer
    private int lastHH = 0; // half height
    private int lastHW = 0; // half width

    private static void setOpacity(Element el, int opacity) {
        if (BrowserInfo.get().isIE()) {
            DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity
                    + ")");
        } else {
            DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));
            DOM.setStyleAttribute(el, "-moz-opacity", "" + (opacity / 100.0));
            DOM.setStyleAttribute(el, "-webkit-opacity", "" + (opacity / 100.0));
        }
    }

    private int[] translate(double lat, double lon) {
        // First calculate 0..1x0..1 "normalized" mercator (thanks Mapki)
        if (lon > 180) {
            lon -= 360;
        }
        lon /= 360;
        lon += 0.5;
        lat = 0.5 - ((Math.log(Math.tan((Math.PI / 4)
                + (Math.toRadians(0.5 * lat)))) / Math.PI) / 2.0);

        // Let map width dictate
        lat = lat * mapWidth; // should be height
        lon = lon * mapWidth;

        int[] res = { (int) Math.round(lon), (int) Math.round(lat) - topCrop };
        return res;
    }

    public void addHighligt(final Highlight h) {
        if (loaded) {
            showHighlight(h);
        } else {
            queueHighlight(h);
        }
    }

    private void queueHighlight(Highlight h) {
        if (highlightQueue == null) {
            highlightQueue = new LinkedList<Highlight>();
        }
        highlightQueue.add(h);
    }

    private void showHighlight(final Highlight h) {
        final int[] px = translate(h.latitude, h.longitude);
        final Image img = new Image();
        img.setUrl((String) h.getImg());
        add(img, px[0] - lastHH, px[1] - lastHW);
        setWidgetPosition(img, px[0] - lastHH, px[1] - lastHW);
        if (h.getZindex() != ZINDEX_UNDER) {
            DOM.setIntStyleAttribute(img.getElement(), "zIndex", h.getZindex());
        }
        Timer fader = new Timer() {
            private final long start = new Date().getTime();

            @Override
            public void run() {
                // adjust position
                lastHH = (int) Math.round(0.5 * img.getHeight());
                lastHW = (int) Math.round(0.5 * img.getWidth());
                setWidgetPosition(img, px[0] - lastHH, px[1] - lastHW);

                // fade
                long now = new Date().getTime();
                long timeElapsed = now - start;
                float remainingFraction = 1 - timeElapsed
                        / (float) h.getFadeMsec();
                int opacity = (int) (100 * remainingFraction);
                if (opacity <= 0) {
                    cancel();
                    remove(img);
                } else {
                    setOpacity(img.getElement(), opacity);
                }
            }
        };
        fader.scheduleRepeating(FADE_ANIMATION_INTERVAL);
    }

    public int getMapWidth() {
        return mapWidth;
    }

    public int getMapHeight() {
        return mapHeight;
    }

    @Override
    public void setHeight(String height) {
        this.height = height;
        if ("".equals(height)) {
            super.setHeight(mapImg.getHeight() + "px");
        } else {
            super.setHeight(height);
        }
    }

    public String getHeight() {
        return this.height;
    }

    @Override
    public void setWidth(String width) {
        this.width = width;
        if ("".equals(width)) {
            super.setWidth(mapImg.getWidth() + "px");
        } else {
            super.setWidth(width);
        }

    }

    public String getWidth() {
        return this.width;
    }

    public int getTopCrop() {
        return topCrop;
    }

    public void setTopCrop(int topCrop) {
        this.topCrop = topCrop;
    }

    public void setMapUrl(String url) {
        loaded = url != null && !url.equals(mapImg.getUrl());
        mapImg.setUrl(url);
    }

    public String getMapUrl() {
        return mapImg.getUrl();
    }

    public static class Highlight {
        private double latitude;
        private double longitude;
        private String img = null;
        private long fadeMsec = 5000;
        private int zindex = 0;

        public Highlight() {
            this(0, 0);
        }

        public Highlight(double latitude, double longitude) {
            this.latitude = latitude;
            this.longitude = longitude;
        }

        /**
         * Represents one highlight on the map. Used both on client and server
         * for simplicity, which introduces a gotcha: the image is represented
         * as a Resource on the server (Vaadin), a String on the client (GWT).
         * 
         * @param latitude
         * @param longitude
         * @param imageURL
         *            URL to highlight image
         * @param fadeMsec
         * @param zindex
         */
        public Highlight(double latitude, double longitude, String imageURL,
                long fadeMsec, int zindex) {
            this.latitude = latitude;
            this.longitude = longitude;
            this.img = imageURL;
            this.fadeMsec = fadeMsec;
            this.zindex = zindex;
        }

        public double getLatitude() {
            return latitude;
        }

        public double getLongitude() {
            return longitude;
        }

        public void setLatitude(double latitude) {
            this.latitude = latitude;
        }

        public void setLongitude(double longitude) {
            this.longitude = longitude;
        }

        public String getImg() {
            return img;
        }

        public void setImg(String img) {
            this.img = img;
        }

        public long getFadeMsec() {
            return fadeMsec;
        }

        public void setFadeMsec(long fadeMsec) {
            this.fadeMsec = fadeMsec;
        }

        public int getZindex() {
            return zindex;
        }

        public void setZindex(int zindex) {
            this.zindex = zindex;
        }

    }

    public interface MapLoadListener {
        public void loaded();
    }

}
